#include "stdafx.h"
#include "javaReflection.h"
#include "jniEnvironment.h"
#include "java.h"
#include "jniString.h"
#include "jniReference.h"
#include "jtf.h"
#include "dispTestObject.h"
#include "dispTestObjectList.h"
#include "customException.h"
#include <cassert>
#include <vector>
#include <sstream>

#define UNKNOWN_TYPE L"unknown"

jobject CJavaReflection::FindMethod(jobject instance, const std::wstring& methodName, jobjectArray jobjarrParams, UINT numberOfArguments)
{
  CJniEnvironment jni;
  JavaMethodClass javaMethod;
  JavaObjectClass javaObject;
  JavaClassClass javaClass;
  JavaBooleanClass javaBoolean;
  JavaIntegerClass javaInteger;

  CJniLocalReference<jobject> jobjClass = javaObject.GetClass.Invoke(instance);
  CJniLocalReference<jobjectArray> jobjarrAllMethods = (jobjectArray)javaClass.GetMethods.Invoke(jobjClass.Get());
  jsize methodsFound = jni.Env()->GetArrayLength(jobjarrAllMethods.Get());
  for (jsize i = 0; i < methodsFound; ++i)
  {
    CJniLocalReference<jobject> jobjMethod = jni.Env()->GetObjectArrayElement(jobjarrAllMethods.Get(), i);
    CJniString currentMethodName = javaMethod.GetName.Invoke(jobjMethod.Get());
    if (::_wcsicmp(currentMethodName.WString().c_str(), methodName.c_str()))
      continue;
    CJniLocalReference<jobjectArray> jobjarrParameterTypes = (jobjectArray) javaMethod.GetParameterTypes.Invoke(jobjMethod.Get());
    jsize currentNumberOfArguments = jni.Env()->GetArrayLength(jobjarrParameterTypes.Get());
    if (numberOfArguments != currentNumberOfArguments)
      continue;
    // validate param types
    bool typesDontMatch = false;
    for (jsize paramIndex = 0; paramIndex < currentNumberOfArguments; paramIndex++)
    {
      CJniLocalReference<jobject> jobjParamObject = jni.Env()->GetObjectArrayElement(jobjarrParams, paramIndex);
      CJniLocalReference<jclass> jclassParamClass = (jclass)jni.Env()->GetObjectArrayElement(jobjarrParameterTypes.Get(), paramIndex);

      // Fix boolean and int handling
      if (jni.Env()->IsAssignableFrom(javaBoolean.TYPE, jclassParamClass.Get()))
        jclassParamClass = javaBoolean.Class();
      if (jni.Env()->IsAssignableFrom(javaInteger.TYPE, jclassParamClass.Get()))
        jclassParamClass = javaInteger.Class();

      if (jni.Env()->IsInstanceOf(jobjParamObject.Get(), jclassParamClass.Get()) == JNI_FALSE)
      {
        typesDontMatch = true;
        break;
      }
    }
    if (typesDontMatch)
      continue;
    return jobjMethod.Detach();
  }
  return 0;
}

jobjectArray CJavaReflection::ConvertParameters(DISPPARAMS* pdispparams)
{
  CJniEnvironment jni;
  JavaClassClass javaClass;
  JavaMethodClass javaMethod;
  JavaObjectClass javaObject;

  CJniLocalReference<jobjectArray> jobjarrParameters = jni.Env()->NewObjectArray(pdispparams->cArgs, javaObject.Class(), 0);
  for (jsize i = 0; i < (jsize)pdispparams->cArgs; i++)
  {
    // order of arguments is inverted
    CJniLocalReference<jobject> jobjParameterValue(VariantToJobject(pdispparams->rgvarg + pdispparams->cArgs - 1 - i));
    if (!jobjParameterValue)
      return 0;
    jni.Env()->SetObjectArrayElement(jobjarrParameters.Get(), i, jobjParameterValue.Get());
  }
  return jobjarrParameters.Detach();
}

std::wstring CJavaReflection::GetReadableType(jclass clazz) 
{
  CJniEnvironment jni;
  JavaStringClass javaString;
  JavaIntegerClass javaInt;
  JavaBooleanClass javaBool;
  JtfAbstractTestObjectClass abstractTestObj;
  JavaClassClass javaClass;
  if (javaClass.IsAssignableFrom.Invoke(javaString.Class(), clazz))
    return L"String";
  else if (javaClass.IsAssignableFrom.Invoke(javaInt.Class(), clazz) || javaClass.IsAssignableFrom.Invoke(javaInt.TYPE, clazz))
    return L"Integer";
  else if (javaClass.IsAssignableFrom.Invoke(javaBool.Class(), clazz) || javaClass.IsAssignableFrom.Invoke(javaBool.TYPE, clazz))
    return L"Boolean";
  else if (javaClass.IsAssignableFrom.Invoke(abstractTestObj.Class(), clazz))
    return L"TestObject";
  else 
    return UNKNOWN_TYPE;
}

HRESULT CJavaReflection::GetMethodList(jobject jobjTestObject, VARIANT* pvarResult)
{
  if (!pvarResult)
    return S_OK;
  CJniEnvironment jni;
  JavaMethodClass javaMethod;
  JavaObjectClass javaObject;
  JavaClassClass javaClass;

  CJniLocalReference<jobject> jobjClass = javaObject.GetClass.Invoke(jobjTestObject);
  CJniLocalReference<jobjectArray> jobjarrAllMethods = (jobjectArray) javaClass.GetMethods.Invoke(jobjClass.Get());
  jsize methodsFound = jni.Env()->GetArrayLength(jobjarrAllMethods.Get());
  CDispTestObjectList* pList = dynamic_cast<CDispTestObjectList*>(mpObjectFactory->CreateDispTestObjectList(0));
  assert(pList);
  for (jsize i = 0; i < methodsFound; ++i)
  {
    std::wstringstream ss;
    // method name
    CJniLocalReference<jobject> jobjMethod = jni.Env()->GetObjectArrayElement(jobjarrAllMethods.Get(), i);
    CJniString sMethodName = javaMethod.GetName.Invoke(jobjMethod.Get());
    ss << sMethodName.WString() << L"(";
    // argument types
    CJniLocalReference<jobjectArray> jobjarrParameterTypes = (jobjectArray)javaMethod.GetParameterTypes.Invoke(jobjMethod.Get());
    jsize numberOfArguments = jni.Env()->GetArrayLength(jobjarrParameterTypes.Get());
    bool comma = false;
    for (jsize paramIndex = 0; paramIndex < numberOfArguments; paramIndex++)
    {
      jclass jclassParamClass = (jclass)jni.Env()->GetObjectArrayElement(jobjarrParameterTypes.Get(), paramIndex);
      ss << (comma ? L"," : L"") << GetReadableType(jclassParamClass);
      comma = true;
    }
    ss << L")";
    // only add methods with valid data types
    if (ss.str().find(UNKNOWN_TYPE) != std::wstring::npos)
      continue;
    CComVariant varValue = ss.str().c_str();
    pList->Add(varValue);    
  }
  pvarResult->vt = VT_DISPATCH;
  pvarResult->pdispVal = pList;
  return S_OK;
}

jobject CJavaReflection::VariantToJobject(VARIANT* pVariant)
{
  CJniEnvironment jni;
  switch (pVariant->vt)
  {
  case VT_BSTR:
    {
      CJniString str(pVariant->bstrVal);
      return str.Detach();
    }
  case VT_INT:
  case VT_I4:
    {
      JavaIntegerClass javaInteger;
      return javaInteger.Constructor.New(VARGS, pVariant->intVal);
    }
  case VT_BOOL:
    {
      JavaBooleanClass javaBoolean;
      return javaBoolean.Constructor.New(VARGS, pVariant->boolVal);
    }
  }
  return 0;
}

void CJavaReflection::JobjectToVariant(jobject jobjObjectValue, VARIANT* pVariant)
{
  if (!pVariant)
    return;
  CJniEnvironment jni;
  JavaObjectClass javaObject;
  JavaIntegerClass javaInteger;
  JavaBooleanClass javaBoolean;
  JavaStringClass javaString;
  JtfAbstractTestObjectClass jtfAbstractTestObject;
  JavaListClass javaList;
  JavaVoidClass javaVoid;
  if (javaVoid.IsInstance(jobjObjectValue))
  {
    // do nothing
  }
  else if (javaInteger.IsInstance(jobjObjectValue))
  {
    jint jiVal = javaInteger.IntValue.Invoke(jobjObjectValue);
    pVariant->vt = VT_INT;
    pVariant->intVal = jiVal;
  }
  else if (javaBoolean.IsInstance(jobjObjectValue))
  {
    jboolean jbVal = javaBoolean.BooleanValue.Invoke(jobjObjectValue);
    pVariant->vt = VT_BOOL;
    pVariant->boolVal = jbVal;
  }
  else if (javaString.IsInstance(jobjObjectValue))
  {
    const jchar* str = jni.Env()->GetStringChars((jstring)jobjObjectValue, 0);
    pVariant->vt = VT_BSTR;
    pVariant->bstrVal = SysAllocString((OLECHAR*)str);
    jni.Env()->ReleaseStringChars((jstring)jobjObjectValue, str);
  }
  else if (jtfAbstractTestObject.IsInstance(jobjObjectValue))
  {
    pVariant->vt = VT_DISPATCH;
    pVariant->pdispVal = mpObjectFactory->CreateDispTestObject(jobjObjectValue);
  }
  else if (javaList.IsInstance(jobjObjectValue))
  {
    pVariant->vt = VT_DISPATCH;
    pVariant->pdispVal = mpObjectFactory->CreateDispTestObjectList(jobjObjectValue);
  }
  else
  {
    // Object.toString() for all other types
    CJniString jniString = javaObject.ToString.Invoke(jobjObjectValue);
    pVariant->vt = VT_BSTR;
    pVariant->bstrVal = SysAllocString((OLECHAR*)jniString.WString().c_str());
  }
}
HRESULT CJavaReflection::InvokeDynamicMethod(jobject jobjTestObject, const std::wstring& methodName, DISPPARAMS* pdispparams, VARIANT* pvarResult, EXCEPINFO* pexcepinfo, UINT* puArgErr)
{
  CJniEnvironment jni;
  CJniLocalReference<jobjectArray> jobjarrParameters = ConvertParameters(pdispparams);
  if (!jobjarrParameters)
    throw CustomException(10000, L"Dynamic Method Invocation", L"Could not convert the arguments.");
  CJniLocalReference<jobject> jobjTargetMethod = FindMethod(jobjTestObject, methodName, jobjarrParameters.Get(), pdispparams->cArgs);
  if (!jobjTargetMethod)
    throw CustomException(10000, L"Dynamic Method Invocation", L"Method not found.");
  JavaMethodClass javaMethod;
  CJniLocalReference<jobject> jobjRetVal = javaMethod.Invoke.Invoke(jobjTargetMethod.Get(), jobjTestObject, jobjarrParameters.Get());
  jni.DoExceptionHandling();
  JobjectToVariant(jobjRetVal.Detach(), pvarResult); // CHECK
  jni.DoExceptionHandling();
  return S_OK;
}
